自定义绘制器
在 Flutter 中手动推进和绘制 Rive 画板。
你可以使用 CustomPainter 自行管理画板的推进和绘制。这将在绘制层面为你提供更多控制,允许你:
- 将多个 Rive 画板绘制到同一个 Flutter Canvas。
- 手动推进画板并控制经过的时间。
- 复用同一个画板实例并多次重绘。
- 在画布上应用更复杂的裁剪、变换或其他绘制/渲染操作。
Flame 游戏引擎 使用下面讨论的技术来渲染 Rive 动画。此示例中的部分代码取自 flame_rive 包。
请注意,这是一个底层 API,在大多数情况下,最好使用
RiveAnimation或Rive组件。
示例代码
以下是一个完整示例, 演示如何手动推进单个画板并将其在网格中多次绘制到同一个 Flutter 画布上。
参见在线 IDE 示例直接运行。

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:rive/math.dart';
import 'package:rive/rive.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyRiveWidget(),
);
}
}
class MyRiveWidget extends StatefulWidget {
const MyRiveWidget({Key? key}) : super(key: key);
@override
State<MyRiveWidget> createState() => _MyRiveWidgetState();
}
class _MyRiveWidgetState extends State<MyRiveWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController =
AnimationController(vsync: this, duration: const Duration(seconds: 10));
RiveArtboardRenderer? _artboardRenderer;
Future<void> _load() async {
// 你需要自行管理将控制器添加到画板,
// 不像 RiveAnimation 组件那样通过简单地提供状态机(或动画)名称来处理大量此类逻辑。
final file = await RiveFile.asset('assets/little_machine.riv');
final artboard = file.mainArtboard.instance();
final controller = StateMachineController.fromArtboard(
artboard,
'State Machine 1',
);
artboard.addController(controller!);
setState(
() => _artboardRenderer = RiveArtboardRenderer(
antialiasing: true,
fit: BoxFit.cover,
alignment: Alignment.center,
artboard: artboard,
),
);
}
@override
void initState() {
super.initState();
_animationController.repeat();
_load();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: _artboardRenderer == null
? const SizedBox()
: CustomPaint(
painter: RiveCustomPainter(
_artboardRenderer!,
repaint: _animationController,
),
child: const SizedBox.expand(), // 使用所有可用空间
),
),
);
}
}
class RiveCustomPainter extends CustomPainter {
final RiveArtboardRenderer artboardRenderer;
RiveCustomPainter(this.artboardRenderer, {super.repaint}) {
_lastTickTime = DateTime.now();
_elapsedTime = Duration.zero;
}
late DateTime _lastTickTime;
late Duration _elapsedTime;
void _calculateElapsedTime() {
final currentTime = DateTime.now();
_elapsedTime = currentTime.difference(_lastTickTime);
_lastTickTime = currentTime;
}
@override
void paint(Canvas canvas, Size size) {
_calculateElapsedTime(); // 计算自上次 tick 后经过的时间。
// 按经过的时间推进画板。
artboardRenderer.advance(_elapsedTime.inMicroseconds / 1000000);
final width = size.width / 3;
final height = size.height / 2;
final artboardSize = Size(width, height);
// 第一行
canvas.save();
artboardRenderer.render(canvas, artboardSize);
canvas.restore();
canvas.save();
canvas.translate(width, 0);
artboardRenderer.render(canvas, artboardSize);
canvas.restore();
canvas.save();
canvas.translate(width * 2, 0);
artboardRenderer.render(canvas, artboardSize);
canvas.restore();
// 第二行
canvas.save();
canvas.translate(0, height);
artboardRenderer.render(canvas, artboardSize);
canvas.restore();
canvas.save();
canvas.translate(width, height);
artboardRenderer.render(canvas, artboardSize);
canvas.restore();
canvas.save();
canvas.translate(width * 2, height);
artboardRenderer.render(canvas, artboardSize);
canvas.restore();
// 绘制完整画布大小
// artboardRenderer.render(canvas, size);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
/// 保存 `Artboard` 实例并将其渲染到 `Canvas`。
///
/// 这是 `RiveAnimation` 组件及其 RenderObject 的简化版本
///
/// 它考虑了 `fit` 和 `alignment` 属性,类似于 `RiveAnimation` 的工作方式。
class RiveArtboardRenderer {
final Artboard artboard;
final bool antialiasing;
final BoxFit fit;
final Alignment alignment;
RiveArtboardRenderer({
required this.antialiasing,
required this.fit,
required this.alignment,
required this.artboard,
}) {
artboard.antialiasing = antialiasing;
}
void advance(double dt) {
artboard.advance(dt, nested: true);
}
late final aabb = AABB.fromValues(0, 0, artboard.width, artboard.height);
void render(Canvas canvas, Size size) {
_paint(canvas, aabb, size);
}
final _transform = Mat2D();
final _center = Mat2D();
void _paint(Canvas canvas, AABB bounds, Size size) {
const position = Offset.zero;
final contentWidth = bounds[2] - bounds[0];
final contentHeight = bounds[3] - bounds[1];
if (contentWidth == 0 || contentHeight == 0) {
return;
}
final x = -1 * bounds[0] -
contentWidth / 2.0 -
(alignment.x * contentWidth / 2.0);
final y = -1 * bounds[1] -
contentHeight / 2.0 -
(alignment.y * contentHeight / 2.0);
var scaleX = 1.0;
var scaleY = 1.0;
canvas.save();
canvas.clipRect(position & size);
switch (fit) {
case BoxFit.fill:
scaleX = size.width / contentWidth;
scaleY = size.height / contentHeight;
break;
case BoxFit.contain:
final minScale =
min(size.width / contentWidth, size.height / contentHeight);
scaleX = scaleY = minScale;
break;
case BoxFit.cover:
final maxScale =
max(size.width / contentWidth, size.height / contentHeight);
scaleX = scaleY = maxScale;
break;
case BoxFit.fitHeight:
final minScale = size.height / contentHeight;
scaleX = scaleY = minScale;
break;
case BoxFit.fitWidth:
final minScale = size.width / contentWidth;
scaleX = scaleY = minScale;
break;
case BoxFit.none:
scaleX = scaleY = 1.0;
break;
case BoxFit.scaleDown:
final minScale =
min(size.width / contentWidth, size.height / contentHeight);
scaleX = scaleY = minScale < 1.0 ? minScale : 1.0;
break;
}
Mat2D.setIdentity(_transform);
_transform[4] = size.width / 2.0 + (alignment.x * size.width / 2.0);
_transform[5] = size.height / 2.0 + (alignment.y * size.height / 2.0);
Mat2D.scale(_transform, _transform, Vec2D.fromValues(scaleX, scaleY));
Mat2D.setIdentity(_center);
_center[4] = x;
_center[5] = y;
Mat2D.multiply(_transform, _transform, _center);
canvas.translate(
size.width / 2.0 + (alignment.x * size.width / 2.0),
size.height / 2.0 + (alignment.y * size.height / 2.0),
);
canvas.scale(scaleX, scaleY);
canvas.translate(x, y);
artboard.draw(canvas);
canvas.restore();
}
}
RiveArtboardRenderer 类取自 Flame Rive 包,可以作为理解 Rive 如何使用 Alignment 和 BoxFit 将画板布局到画布上的起点。
关键步骤如下:
- 从
RiveFile访问画板并附加任何 Rive 动画控制器(StateMachineController)。动画可以像往常一样通过控制器进行控制。 - 创建 Flutter
CustomPaint组件以访问 Flutter 画布。 - 使用
AnimationController(或 Ticker/Listener)强制RiveCustomPainter在每一帧重新绘制。 - 计算动画 tick 之间的经过时间。
- 使用
artboard.advance(dt, nested: true);推进画板,其中dt是经过的时间(增量时间)。 - 使用
artboard.draw(canvas);将画板绘制到画布上。
其余代码负责布局和尺寸调整。
其他示例
在此示例中使用了单个画板,但也可以将多个画板实例(来自相同或不同的 Rive 文件)绘制到同一个画布上。
参见此可编辑示例,它展示了如何将两个不同的画板(为每个僵尸创建唯一的画板实例)绘制到同一画布上。每个画板都有一个 数字输入,用于在不同皮肤之间切换:
Rive Flutter 自定义绘制器 - 多个画板:

请注意,在画板上调用
.instance()会创建一个可以独立推进的唯实例。